classdef ThemeManager < handle
    % ThemeManager - 统一主题管理服务
    % 
    % 功能：
    %   - 自动识别并应用主题到GUI控件
    %   - 支持YAML配置文件
    %   - 支持排除机制
    %   - 兼容MATLAB 2025a原生主题和手动主题
    %
    % 遵循规则：
    %   - 规则第4条：架构分离原则 - 主题逻辑与GUI解耦
    %   - 规则第2条：错误处理原则 - 明确报告所有错误
    %   - 规则第6条：调试友好原则 - 不随意添加try-catch
    %
    % 使用方法：
    %   % 方式1：使用默认配置
    %   themeMgr = ThemeManager();
    %   themeMgr.applyTheme(app.MainFigure, 'dark');
    %
    %   % 方式2：自定义配置文件
    %   themeMgr = ThemeManager('ConfigPath', 'my_theme_config.yml');
    %   themeMgr.applyTheme(app.MainFigure, 'light');
    %
    %   % 方式3：添加排除项
    %   themeMgr = ThemeManager();
    %   themeMgr.addExclusion('PropertyName', 'mySpecialButton');
    %   themeMgr.applyTheme(app.MainFigure, 'dark');
    %
    % 作者：MBD Platform Team
    % 日期：2025-10-18
    % 版本：1.0
    
    properties (Access = private)
        Config          % 主题配置结构
        ConfigPath      % 配置文件路径
        Verbose         % 是否显示详细日志
        AppliedCount    % 已应用主题的控件数量
        SkippedCount    % 跳过的控件数量
    end
    
    methods
        function obj = ThemeManager(varargin)
            % 构造函数
            %
            % 参数：
            %   'ConfigPath' - YAML配置文件路径（可选）
            %   'Verbose'    - 是否显示详细日志（默认false）
            
            % 解析输入参数
            p = inputParser;
            addParameter(p, 'ConfigPath', '', @ischar);
            addParameter(p, 'Verbose', false, @islogical);
            parse(p, varargin{:});
            
            obj.Verbose = p.Results.Verbose;
            
            % 确定配置文件路径
            if isempty(p.Results.ConfigPath)
                % 使用默认配置文件路径
                currentFile = mfilename('fullpath');
                currentDir = fileparts(currentFile);
                obj.ConfigPath = fullfile(currentDir, 'theme_config.yml');
            else
                obj.ConfigPath = p.Results.ConfigPath;
            end
            
            % 加载配置
            obj.loadConfig();
            
            % 初始化计数器
            obj.AppliedCount = 0;
            obj.SkippedCount = 0;
        end
        
        function applyTheme(obj, figure, themeName)
            % 应用主题到指定的figure及其所有子组件
            %
            % 参数：
            %   figure    - MATLAB figure或app的MainFigure
            %   themeName - 主题名称: 'light', 'dark', 'tech' 或配置文件中定义的任何主题
            %
            % 返回：
            %   无返回值，直接修改figure的外观
            
            % 验证输入
            if ~isvalid(figure)
                error('ThemeManager:InvalidFigure', 'Figure无效或已被删除');
            end
            
            % 验证主题名称（基于项目编码规则第2条：错误处理原则）
            themeName = obj.validateThemeName(themeName);
            
            % 重置计数器
            obj.AppliedCount = 0;
            obj.SkippedCount = 0;
            
            % 记录开始时间
            startTime = tic;
            
            if obj.Verbose
                fprintf('🎨 开始应用 %s 主题...\n', themeName);
            end
            
            % 1. 尝试使用MATLAB 2025a原生主题（如果启用）
            if obj.Config.options.use_native_theme && isprop(figure, 'Theme')
                try
                    figure.Theme = themeName;
                    figure.ThemeMode = "manual";
                    if obj.Verbose
                        fprintf('✓ MATLAB 2025a原生主题已应用\n');
                    end
                catch ME
                    if obj.Verbose
                        fprintf('⚠️ 原生主题设置失败: %s\n', ME.message);
                    end
                end
            end
            
            % 2. 手动应用主题到所有控件（确保兼容性）
            obj.applyThemeToComponent(figure, themeName, []);
            
            % 3. 递归应用到所有子组件
            if obj.Config.options.recursive
                obj.applyThemeRecursive(figure, themeName);
            end
            
            % 完成
            elapsedTime = toc(startTime);
            if obj.Verbose
                fprintf('✓ 主题应用完成！\n');
                fprintf('  - 已应用: %d 个控件\n', obj.AppliedCount);
                fprintf('  - 已跳过: %d 个控件\n', obj.SkippedCount);
                fprintf('  - 耗时: %.2f 秒\n', elapsedTime);
            end
        end

        function palette = getMessagePalette(obj, themeAlias)
            themeName = obj.validateThemeName(themeAlias);
            palette = obj.getDefaultMessagePalette();

            themeConfig = obj.getThemeConfig(themeName);
            if isfield(themeConfig, 'MessageLevels')
                levels = themeConfig.MessageLevels;
                palette.info = obj.resolveColorValue(levels, 'info', palette.info);
                palette.warning = obj.resolveColorValue(levels, 'warning', palette.warning);
                palette.error = obj.resolveColorValue(levels, 'error', palette.error);
                palette.success = obj.resolveColorValue(levels, 'success', palette.success);
                palette.general = obj.resolveColorValue(levels, 'general', palette.info);
            end
        end
        
        function addExclusion(obj, type, value)
            % 添加排除项
            %
            % 参数：
            %   type  - 排除类型: 'PropertyName', 'Tag', 'ClassName'
            %   value - 要排除的值
            
            switch type
                case 'PropertyName'
                    if ~any(strcmp(obj.Config.exclusions.PropertyNames, value))
                        obj.Config.exclusions.PropertyNames{end+1} = value;
                    end
                case 'Tag'
                    if ~any(strcmp(obj.Config.exclusions.Tags, value))
                        obj.Config.exclusions.Tags{end+1} = value;
                    end
                case 'ClassName'
                    if ~any(strcmp(obj.Config.exclusions.ClassNames, value))
                        obj.Config.exclusions.ClassNames{end+1} = value;
                    end
                otherwise
                    error('ThemeManager:InvalidExclusionType', ...
                        '排除类型必须是: PropertyName, Tag, 或 ClassName');
            end
        end
        
        function stats = getStatistics(obj)
            % 获取主题应用统计信息
            %
            % 返回：
            %   stats - 包含统计信息的结构体
            
            stats = struct(...
                'AppliedCount', obj.AppliedCount, ...
                'SkippedCount', obj.SkippedCount, ...
                'TotalCount', obj.AppliedCount + obj.SkippedCount);
        end
        
        function validatedName = validateThemeName(obj, themeName)
            % 验证并返回标准主题名称
            % 基于项目编码规则第2条：错误处理原则 - 明确报告错误
            %
            % 参数：
            %   themeName - 主题名称（如 'light', 'dark'）
            %
            % 返回：
            %   validatedName - 验证后的标准主题名称
            %
            % 异常：
            %   如果主题名称无效，抛出错误
            
            if ~obj.isValidTheme(themeName)
                availableThemes = obj.getAvailableThemes();
                themeNames = {availableThemes.name};
                error('ThemeManager:InvalidThemeName', ...
                    '主题名称 ''%s'' 无效。可用主题: %s', ...
                    themeName, strjoin(themeNames, ', '));
            end
            
            validatedName = themeName;
        end
        
        function themeList = getAvailableThemes(obj)
            % 获取配置文件中所有可用的主题列表
            % 基于项目编码规则第4条：架构分离原则 - 业务逻辑在Service Layer实现
            % 基于项目编码规则第2条：错误处理原则 - 从配置文件读取，不硬编码
            %
            % 返回：
            %   themeList - 包含主题信息的结构体数组
            %               .name - 主题内部名称（如 'dark', 'light', 'tech'）
            %               .displayName - 主题显示名称（中文）
            %
            % 示例：
            %   themeMgr = ThemeManager();
            %   themes = themeMgr.getAvailableThemes();
            %   % themes(1).name = 'light'
            %   % themes(1).displayName = '浅色模式'
            
            % 检查配置是否已加载
            if isempty(obj.Config) || ~isfield(obj.Config, 'themes')
                error('ThemeManager:ConfigNotLoaded', '配置文件未正确加载');
            end
            
            % 获取主题名称
            themeNames = fieldnames(obj.Config.themes);
            
            % 构建主题列表
            themeList = struct('name', {}, 'displayName', {});
            for i = 1:numel(themeNames)
                themeName = themeNames{i};
                themeConfig = obj.Config.themes.(themeName);
                
                % 从配置文件读取显示名称（基于项目编码规则第2条）
                if isfield(themeConfig, 'displayName')
                    displayName = themeConfig.displayName;
                else
                    % 如果配置文件中没有定义，使用默认格式（首字母大写）
                    displayName = [upper(themeName(1)), themeName(2:end)];
                end
                
                themeList(i).name = themeName;
                themeList(i).displayName = displayName;
            end
        end
        
        function displayName = getThemeDisplayName(obj, themeName)
            % 获取指定主题的显示名称
            % 基于项目编码规则第4条：架构分离原则
            %
            % 参数：
            %   themeName - 主题内部名称（如 'dark', 'light'）
            %
            % 返回：
            %   displayName - 主题的显示名称（中文）
            %
            % 示例：
            %   themeMgr = ThemeManager();
            %   displayName = themeMgr.getThemeDisplayName('dark');
            %   % 返回: '深色模式'
            
            themeList = obj.getAvailableThemes();
            idx = find(strcmp({themeList.name}, themeName), 1);
            
            if isempty(idx)
                error('ThemeManager:InvalidThemeName', ...
                    '主题名称 ''%s'' 无效', themeName);
            end
            
            displayName = themeList(idx).displayName;
        end
    end
    
    methods (Access = private)
        function loadConfig(obj)
            % 加载YAML配置文件
            
            if ~exist(obj.ConfigPath, 'file')
                error('ThemeManager:ConfigNotFound', ...
                    '配置文件未找到: %s', obj.ConfigPath);
            end
            
            try
                % 使用项目自带的yamlread工具（基于SnakeYAML）
                % 这个工具兼容所有MATLAB版本（R2023b及更早版本）
                obj.Config = yamlread(obj.ConfigPath, true);
                
                if obj.Verbose
                    fprintf('✓ 配置文件已加载: %s\n', obj.ConfigPath);
                end
            catch ME
                error('ThemeManager:ConfigLoadError', ...
                    '配置文件加载失败: %s\n错误: %s', obj.ConfigPath, ME.message);
            end
        end
        
        function isValid = isValidTheme(obj, themeName)
            % 检查主题名称是否有效
            % 基于项目编码规则第2条：错误处理原则 - 明确验证输入
            %
            % 参数：
            %   themeName - 主题名称
            %
            % 返回：
            %   isValid - true表示主题名称有效
            
            themes = obj.getAvailableThemes();
            themeNames = {themes.name};
            isValid = ismember(themeName, themeNames);
        end

        function palette = getDefaultMessagePalette(~)
            palette = struct( ...
                'info', MessageService.COLOR_INFO, ...
                'warning', MessageService.COLOR_WARNING, ...
                'error', MessageService.COLOR_ERROR, ...
                'success', MessageService.COLOR_SUCCESS, ...
                'general', MessageService.COLOR_INFO);
        end

        function themeConfig = getThemeConfig(obj, themeName)
            themes = obj.Config.themes;
            if ~isfield(themes, themeName)
                error('ThemeManager:UnknownTheme', '主题名称"%s"未在配置文件中定义。', themeName);
            end
            themeConfig = themes.(themeName);
        end

        function rgb = resolveColorValue(obj, colorStruct, fieldName, defaultValue)
            if isfield(colorStruct, fieldName)
                rgb = obj.parseColorValue(colorStruct.(fieldName));
            else
                rgb = defaultValue;
            end
        end

        function rgb = parseColorValue(obj, value)
            if isempty(value)
                rgb = MessageService.COLOR_INFO;
                return;
            end

            if ischar(value) || (isstring(value) && isscalar(value))
                rgb = obj.hexToRgb(char(value));
            elseif isnumeric(value)
                rgb = obj.normalizeNumericColor(value);
            elseif iscell(value)
                rgb = obj.parseColorValue(value{1});
            else
                rgb = MessageService.COLOR_INFO;
            end
        end

        function rgb = normalizeNumericColor(~, value)
            rgb = value(:)';
            if numel(rgb) ~= 3
                error('ThemeManager:InvalidColorValue', '数值颜色必须包含三个分量。');
            end
            if any(rgb > 1)
                rgb = rgb ./ 255;
            end
        end
        
        function applyThemeRecursive(obj, parent, themeName)
            % 递归遍历所有子组件并应用主题
            %
            % 参数：
            %   parent    - 父组件
            %   themeName - 主题名称
            
            try
                % 获取所有子组件
                children = parent.Children;
                
                % 遍历每个子组件
                for i = 1:numel(children)
                    child = children(i);
                    
                    % 应用主题到当前组件
                    obj.applyThemeToComponent(child, themeName, parent);
                    
                    % 如果有Children属性，继续递归
                    if isprop(child, 'Children') && ~isempty(child.Children)
                        obj.applyThemeRecursive(child, themeName);
                    end
                end
            catch ME
                if obj.Verbose
                    fprintf('⚠️ 递归遍历时出错: %s\n', ME.message);
                end
            end
        end
        
        function applyThemeToComponent(obj, component, themeName, parent)
            % 应用主题到单个组件
            %
            % 参数：
            %   component - 要应用主题的组件
            %   themeName - 主题名称
            %   parent    - 父组件（用于获取app上下文）
            
            % 检查是否应跳过此组件
            if obj.shouldExclude(component, parent)
                obj.SkippedCount = obj.SkippedCount + 1;
                return;
            end
            
            % 获取组件的简单类名
            componentType = obj.getComponentType(component);
            
            % 检查是否是特殊规则组件
            if obj.applySpecialRule(component, themeName, componentType, parent)
                obj.AppliedCount = obj.AppliedCount + 1;
                return;
            end
            
            % 应用标准主题
            if obj.applyStandardTheme(component, themeName, componentType)
                obj.AppliedCount = obj.AppliedCount + 1;
            else
                obj.SkippedCount = obj.SkippedCount + 1;
            end
        end
        
        function shouldExclude = shouldExclude(obj, component, parent)
            % 检查组件是否应被排除
            %
            % 参数：
            %   component - 要检查的组件
            %   parent    - 父组件
            %
            % 返回：
            %   shouldExclude - true表示应排除
            
            shouldExclude = false;
            
            % 1. 检查Tag排除
            if isprop(component, 'Tag') && ~isempty(component.Tag)
                for i = 1:numel(obj.Config.exclusions.Tags)
                    if contains(component.Tag, obj.Config.exclusions.Tags{i})
                        shouldExclude = true;
                        return;
                    end
                end
            end
            
            % 2. 检查ClassName排除
            className = class(component);
            if any(strcmp(obj.Config.exclusions.ClassNames, className))
                shouldExclude = true;
                return;
            end
            
            % 3. 检查PropertyName排除（需要parent是app对象）
            % 这个检查比较复杂，因为需要找到组件在app中的属性名
            % 暂时跳过，可以通过Tag机制实现
        end
        
        function componentType = getComponentType(obj, component)
            % 获取组件的简单类型名称
            %
            % 参数：
            %   component - 组件对象
            %
            % 返回：
            %   componentType - 简单类型名称（如'Button', 'Label'）
            
            fullClassName = class(component);
            
            % 提取类名的最后一部分
            parts = strsplit(fullClassName, '.');
            componentType = parts{end};
        end
        
        function applied = applySpecialRule(obj, component, themeName, componentType, parent)
            % 应用特殊规则
            %
            % 返回：
            %   applied - true表示已应用特殊规则
            
            applied = false;
            
            % 1. 检查标题标签特殊规则
            if obj.Config.special_rules.title_labels.enabled
                if obj.matchesNamePattern(component, parent, ...
                        obj.Config.special_rules.title_labels.name_patterns)
                    
                    theme = obj.Config.special_rules.title_labels.(themeName);
                    obj.applyProperties(component, theme);
                    applied = true;
                    return;
                end
            end
            
            % 2. 检查消息文本区域特殊规则
            if obj.Config.special_rules.message_text_areas.enabled && ...
                    strcmp(componentType, 'TextArea')
                
                if obj.matchesNamePattern(component, parent, ...
                        obj.Config.special_rules.message_text_areas.name_patterns)
                    
                    % 应用标准TextArea主题
                    standardTheme = obj.Config.themes.(themeName).TextArea;
                    obj.applyProperties(component, standardTheme);
                    
                    % 添加emoji消息
                    if obj.Config.special_rules.message_text_areas.apply_emoji
                        msgConfig = obj.Config.special_rules.message_text_areas.(themeName);
                        component.Value = sprintf('%s %s', msgConfig.emoji, msgConfig.message);
                    end
                    
                    applied = true;
                    return;
                end
            end
        end
        
        function applied = applyStandardTheme(obj, component, themeName, componentType)
            % 应用标准主题
            %
            % 返回：
            %   applied - true表示已成功应用
            
            applied = false;
            
            % 检查配置中是否有此类型的主题定义
            if ~isfield(obj.Config.themes.(themeName), componentType)
                return;
            end
            
            % 获取该类型的主题配置
            themeConfig = obj.Config.themes.(themeName).(componentType);
            
            % 应用属性
            applied = obj.applyProperties(component, themeConfig);
        end
        
        function success = applyProperties(obj, component, propStruct)
            % 应用属性到组件
            %
            % 参数：
            %   component  - 组件对象
            %   propStruct - 包含属性的结构体
            %
            % 返回：
            %   success - true表示至少应用了一个属性
            
            success = false;
            fields = fieldnames(propStruct);
            
            for i = 1:numel(fields)
                propName = fields{i};
                propValue = propStruct.(propName);
                
                % 检查组件是否有此属性
                if isprop(component, propName)
                    try
                        % 处理颜色属性（可能是十六进制字符串）
                        if contains(propName, 'Color') && ischar(propValue)
                            propValue = obj.hexToRgb(propValue);
                        end
                        
                        % 将cell数组转换为数值数组（YAML解析问题）
                        if iscell(propValue) && isnumeric(propValue{1})
                            propValue = cell2mat(propValue);
                        end
                        
                        component.(propName) = propValue;
                        success = true;
                    catch ME
                        if obj.Verbose
                            fprintf('⚠️ 属性设置失败 [%s.%s]: %s\n', ...
                                class(component), propName, ME.message);
                        end
                    end
                end
            end
        end
        
        function matches = matchesNamePattern(obj, component, parent, patterns)
            % 检查组件名称是否匹配模式
            %
            % 返回：
            %   matches - true表示匹配
            
            matches = false;
            
            % 尝试通过Tag匹配
            if isprop(component, 'Tag') && ~isempty(component.Tag)
                for i = 1:numel(patterns)
                    if contains(component.Tag, patterns{i})
                        matches = true;
                        return;
                    end
                end
            end
            
            % 尝试通过Text属性匹配（某些控件）
            if isprop(component, 'Text') && ~isempty(component.Text)
                for i = 1:numel(patterns)
                    if contains(component.Text, patterns{i})
                        matches = true;
                        return;
                    end
                end
            end
        end
        
        function rgb = hexToRgb(~, hexStr)
            % 将十六进制颜色字符串转换为RGB数组
            %
            % 参数：
            %   hexStr - 十六进制颜色字符串（如 '#3498db' 或 '3498db'）
            %
            % 返回：
            %   rgb - 1x3 RGB数组，值范围[0, 1]
            %
            % 示例：
            %   hexToRgb('#3498db')  -> [0.2039, 0.5961, 0.8588]
            %   hexToRgb('#FFFFFF')  -> [1, 1, 1]
            %   hexToRgb('#000000')  -> [0, 0, 0]
            
            % 移除可能的 # 前缀
            if hexStr(1) == '#'
                hexStr = hexStr(2:end);
            end
            
            % 验证长度
            if length(hexStr) ~= 6
                error('ThemeManager:InvalidHexColor', ...
                    '十六进制颜色必须是6位字符（如 #3498db）');
            end
            
            % 转换为RGB（0-255）
            r = hex2dec(hexStr(1:2));
            g = hex2dec(hexStr(3:4));
            b = hex2dec(hexStr(5:6));
            
            % 归一化到 [0, 1]
            rgb = [r, g, b] / 255;
        end
    end
    
    methods (Static, Access = public)
        function applyThemeToDialog(dialogFigure, themeName)
            % 应用主题到单个对话框或figure
            % 这是一个静态方法，可以在不创建ThemeManager实例的情况下调用
            % 基于项目编码规则第4条：架构分离原则
            %
            % 参数：
            %   dialogFigure - 对话框figure句柄
            %   themeName - 主题名称（'light', 'dark', 'tech'等）
            %
            % 示例：
            %   dlg = uifigure('Name', 'My Dialog');
            %   ThemeManager.applyThemeToDialog(dlg, 'dark');
            
            % 创建临时ThemeManager实例
            themeMgr = ThemeManager('Verbose', false);
            
            % 应用主题
            themeMgr.applyTheme(dialogFigure, themeName);
        end
    end
end

